//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;

namespace Microsoft.Research.DataLayer
{
    /// <summary>
    /// Base class for connecting to a repository from the registry. This base class
    /// is for repositories which do not have code support to perform complex
    /// repository tasks like join, search etc.
    /// </summary>
    public abstract class NoCodeBehind : Connection
    {
        /// <summary>
        /// Performs operations on a single object
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="action"></param>
        /// <param name="paramList"></param>
        public override void Object(Connection.Storage obj, Connection.Action action, ref List<Parameter> paramList)
        {
            List<IObject> dummy;
            Object(obj, action, ref paramList, null, out dummy);
        }

        /// <summary>
        /// Performs operations on a single object
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="action"></param>
        /// <param name="paramList"></param>
        /// <param name="creator"></param>
        /// <param name="results"></param>
        public override void Object(Connection.Storage obj, Connection.Action action, ref List<Parameter> paramList, Connection.ObjectCreator creator, out List<IObject> results)
        {
            results = null;
            switch (action)
            {
                case Action.Add:
                    {
                        Guid objId = Guid.NewGuid();
                        Parameter createdBy = new Parameter("CreatedBy", Parameter.ParamType.String, Parameter.ParamDirection.In, Environment.UserName);
                        Parameter createdDate = new Parameter("CreatedDate", Parameter.ParamType.DateTime, Parameter.ParamDirection.Out, DateTime.Now);
                        paramList.Add(createdBy);
                        paramList.Add(createdDate);
                        Parameter updatedBy = new Parameter("UpdatedBy", Parameter.ParamType.String, Parameter.ParamDirection.In, Environment.UserName);
                        Parameter updatedDate = new Parameter("UpdatedDate", Parameter.ParamType.DateTime, Parameter.ParamDirection.Out, DateTime.Now);
                        paramList.Add(updatedBy);
                        paramList.Add(updatedDate);

                        InsertRecord(obj, null, objId, ref paramList);
                        break;
                    }
                case Action.Get:
                    RetrieveRecordByName(obj, ref paramList);
                    break;
                case Action.GetByID:
                    RetrieveRecordByID(obj, ref paramList);
                    break;
                case Action.GetAll:
                    RetrieveAllRecords(obj, creator, out results);
                    break;
                case Action.Search:
                    Parameter textParam = Parameter.GetParamByName("Text", paramList);
                    ManualSearchRecords(obj, (string)textParam.Value, creator, out results);
                    break;
                case Action.Update:
                    {
                        Parameter updatedBy = new Parameter("UpdatedBy", Parameter.ParamType.String, Parameter.ParamDirection.In, Environment.UserName);
                        Parameter updatedDate = new Parameter("UpdatedDate", Parameter.ParamType.DateTime, Parameter.ParamDirection.Out, DateTime.Now);
                        paramList.Add(updatedBy);
                        paramList.Add(updatedDate);

                        UpdateRecord(obj, paramList);
                        break;
                    }
                case Action.Delete:
                    DeleteRecord(obj, paramList);
                    break;
                default:
                    throw new NotImplementedException("Not implemented");
            }
        }

        /// <summary>
        /// Performs actions on a relation, given 2 objects listed
        /// </summary>
        /// <param name="action"></param>
        /// <param name="obj1"></param>
        /// <param name="obj2"></param>
        /// <param name="query"></param>
        protected void RelationAction(Connection.Action action, IObject obj1, IObject obj2, string query)
        {
            switch (action)
            {
                case Action.Add:
                    {
                        // We do not care for this ID.
                        Guid dummyRelId = Guid.NewGuid();

                        Connection.Storage obj1Type = obj1.ObjType;
                        Connection.Storage obj2Type = obj2.ObjType;

                        List<Parameter> paramList = new List<Parameter>();
                        paramList.Add(new Parameter("ID", Parameter.ParamType.Guid, Parameter.ParamDirection.In, dummyRelId));

                        if (obj2.ObjType.IsSelfRelation)
                            paramList.Add(new Parameter(string.Format("{0}_1ID", obj1.ObjType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, obj1.ID));
                        else
                            paramList.Add(new Parameter(string.Format("{0}ID", obj1.ObjType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, obj1.ID));

                        if (obj2 is IExtraRelationData)
                        {
                            // It is a standalone object
                            IExtraRelationData extra = (IExtraRelationData)obj2;
                            paramList.AddRange(extra.ExtraData);

                            if (obj2.ObjType.IsSelfRelation)
                                paramList.Add(new Parameter(string.Format("{0}_2ID", obj1.ObjType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, extra.ChildID));
                            else
                                paramList.Add(new Parameter(string.Format("{0}ID", extra.ChildType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, extra.ChildID));

                            // Remap the type to look like an object.
                            obj1Type = obj2.ObjType;
                            obj2Type = null;
                        }
                        else
                        {
                            // Normal N-N relationship
                            if (obj2.ObjType.IsSelfRelation)
                                paramList.Add(new Parameter(string.Format("{0}_2ID", obj2.ObjType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, obj2.ID));
                            else
                                paramList.Add(new Parameter(string.Format("{0}ID", obj2.ObjType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, obj2.ID));
                        }

                        Parameter createdBy = new Parameter("CreatedBy", Parameter.ParamType.String, Parameter.ParamDirection.In, Environment.UserName);
                        Parameter createdDate = new Parameter("CreatedDate", Parameter.ParamType.DateTime, Parameter.ParamDirection.Out);
                        paramList.Add(createdBy);
                        paramList.Add(createdDate);

                        Parameter updatedBy = new Parameter("UpdatedBy", Parameter.ParamType.String, Parameter.ParamDirection.In, Environment.UserName);
                        Parameter updatedDate = new Parameter("UpdatedDate", Parameter.ParamType.DateTime, Parameter.ParamDirection.Out);
                        paramList.Add(updatedBy);
                        paramList.Add(updatedDate);

                        InsertRecord(obj1Type, obj2Type, dummyRelId, ref paramList);
                    }
                    break;

                case Action.Update:
                    {
                        if (obj2 is IExtraRelationData)
                        {
                            IExtraRelationData extra = (IExtraRelationData)obj2;

                            // Find the real ID for the record
                            string objKind = ObjectKind(obj2.ObjType);
                            //string query = string.Format(@"from e in entities where e.Kind==""{0}"" && e[""{1}ID""]==""{2}"" && e[""{3}ID""]==""{4}"" select e",
                            //    objKind,
                            //    obj2.ObjType.IsSelfRelation ? string.Format("{0}_1", obj1.ObjType.Name) : obj1.ObjType.Name, obj1.ID,
                            //    obj2.ObjType.IsSelfRelation ? string.Format("{0}_2", obj1.ObjType.Name) : extra.ChildType.Name, extra.ChildID);
                            DbDataReader affectedRecord = ExecuteRetrieveReader(query);
                            if (affectedRecord.Read())
                            {
                                Guid recordId = affectedRecord.GetGuid(affectedRecord.GetOrdinal("ID"));

                                List<Parameter> mydata = new List<Parameter>();

                                // ID used by the backend
                                mydata.Add(new Parameter("ID", Parameter.ParamType.Guid, Parameter.ParamDirection.In, recordId));

                                if (obj2.ObjType.IsSelfRelation)
                                {
                                    // Uses ID from both items
                                    mydata.Add(new Parameter(string.Format("{0}_1ID", obj1.ObjType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, obj1.ID));
                                    mydata.Add(new Parameter(string.Format("{0}_2ID", obj1.ObjType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, extra.ChildID));
                                }
                                else
                                {
                                    // Uses ID from both items
                                    mydata.Add(new Parameter(string.Format("{0}ID", obj1.ObjType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, obj1.ID));
                                    mydata.Add(new Parameter(string.Format("{0}ID", extra.ChildType), Parameter.ParamType.Guid, Parameter.ParamDirection.In, extra.ChildID));
                                }

                                // All our specific data
                                mydata.AddRange(extra.ExtraData);

                                Parameter updatedBy = new Parameter("UpdatedBy", Parameter.ParamType.String, Parameter.ParamDirection.In, Environment.UserName);
                                Parameter updatedDate = new Parameter("UpdatedDate", Parameter.ParamType.DateTime, Parameter.ParamDirection.Out);
                                mydata.Add(updatedBy);
                                mydata.Add(updatedDate);

                                UpdateRecord(obj2.ObjType, mydata);
                            }
                        }
                    }
                    break;

                case Action.Delete:
                    {
                        if (obj2 is IExtraRelationData)
                        {
                            // Object with data
                            IExtraRelationData extra = (IExtraRelationData)obj2;

                            // Find the real ID for the record
                            string objKind = ObjectKind(obj2.ObjType);
                            //string query = string.Format(@"from e in entities where e.Kind==""{0}"" && e[""{1}ID""]==""{2}"" && e[""{3}ID""]==""{4}"" select e",
                            //    objKind,
                            //    obj2.ObjType.IsSelfRelation ? string.Format("{0}_1", obj1.ObjType.Name) : obj1.ObjType.Name, obj1.ID,
                            //    obj2.ObjType.IsSelfRelation ? string.Format("{0}_2", obj1.ObjType.Name) : extra.ChildType.Name, extra.ChildID);

                            DbDataReader affectedRecord = ExecuteRetrieveReader(query);
                            if (affectedRecord.Read())
                            {
                                Guid recordId = affectedRecord.GetGuid(affectedRecord.GetOrdinal("ID"));
                                DeleteRecord(objKind, recordId);
                            }
                        }
                        else
                        {
                            // Find the real ID for the record
                            string objKind = ObjectKind(obj1.ObjType, obj2.ObjType);
                            //string query = string.Format(@"from e in entities where e.Kind==""{0}"" && e[""{1}ID""]==""{2}"" && e[""{3}ID""]==""{4}"" select e",
                            //    objKind,
                            //    obj2.ObjType.IsSelfRelation ? string.Format("{0}_1", obj1.ObjType.Name) : obj1.ObjType.Name, obj1.ID,
                            //    obj2.ObjType.IsSelfRelation ? string.Format("{0}_2", obj1.ObjType.Name) : obj2.ObjType.Name, obj2.ID);

                            DbDataReader affectedRecord = ExecuteRetrieveReader(query);
                            if (affectedRecord.Read())
                            {
                                Guid recordId = affectedRecord.GetGuid(affectedRecord.GetOrdinal("ID"));
                                DeleteRecord(objKind, recordId);
                            }
                        }
                    }
                    break;
            }
        }

        /// <summary>
        /// Performs operations on a relationship between two objects
        /// </summary>
        /// <param name="action"></param>
        /// <param name="obj1"></param>
        /// <param name="obj2"></param>
        /// <param name="paramList"></param>
        /// <param name="revKey"></param>
        public override void Relation(Connection.Action action, IObject obj1, Connection.Storage obj2, string revKey, ref List<Parameter> paramList)
        {

        }

        /// <summary>
        /// Performs operations on a relationship between two objects
        /// </summary>
        /// <param name="action"></param>
        /// <param name="obj1"></param>
        /// <param name="obj2Type"></param>
        /// <param name="creator"></param>
        /// <param name="results"></param>
        public override void Relation(Connection.Action action, IObject obj1, Connection.Storage obj2Type, Connection.ObjectCreator creator, out List<IObject> results)
        {
            Relation(action, obj1, null, obj2Type, creator, out results);
        }

        /// <summary>
        /// Performs operations on a relationship between two objects
        /// </summary>
        /// <param name="action"></param>
        /// <param name="obj1"></param>
        /// <param name="field"></param>
        /// <param name="obj2Type"></param>
        /// <param name="creator"></param>
        /// <param name="results"></param>
        /// <param name="query"></param>
        protected void RelationAction(Connection.Action action, IObject obj1, string field, Connection.Storage obj2Type, 
            Connection.ObjectCreator creator, out List<IObject> results, string query)
        {
            switch (action)
            {
                case Action.GetByObject:
                    try
                    {
                        if (obj2Type != null && obj2Type.Flags == StorageFlags.RelationObject)
                        {
                            DbDataReader reader = ExecuteRetrieveReader(query);

                            results = new List<IObject>();
                            while (reader.Read())
                            {
                                IObject loadObj = creator(reader, this);
                                results.Add(loadObj);
                            }
                        }
                        else if (field != null)
                        {
                            // Normal 1-N join
                            string objKind = ObjectKind(obj2Type);
                            ExecuteRetrieve(obj2Type, query, creator, out results);
                        }
                        else
                        {
                            // Normal N-N join
                            ManualJoin(action, obj1, obj2Type, creator, out results);
                        }
                    }
                    catch (RecordNotFoundException)
                    {
                        results = new List<IObject>();
                    }
                    break;
                default:
                    throw new NotImplementedException("Not implemented yet");
            }
        }

        /// <summary>
        /// Returns the name of the object to be used to build the query
        /// </summary>
        /// <param name="obj1"></param>
        /// <returns></returns>
        protected string ObjectKind(Connection.Storage obj1)
        {
            return ObjectKind(obj1, null);
        }

        /// <summary>
        /// Returns the name of the object to be used to build the query
        /// </summary>
        /// <param name="obj1"></param>
        /// <param name="obj2"></param>
        /// <returns></returns>
        protected string ObjectKind(Connection.Storage obj1, Connection.Storage obj2)
        {
            // Step 0 : Find right relationship table
            if (obj1 != obj2 && BindingMap.Contains(string.Format("{0}-{1}", obj2, obj1)))
            {
                // We are viewing the problem in "reverse". Flip the table
                return ObjectKind(obj2, obj1);
            }

            string objKind = string.Format("{0}{1}Kind", obj1, obj2 == null ? "" : obj2.ToString());
            return objKind;
        }

        #region Abstract Methods

        /// <summary>
        /// Insert a new object into the store
        /// </summary>
        /// <param name="obj1"></param>
        /// <param name="obj2"></param>
        /// <param name="objId"></param>
        /// <param name="paramList"></param>
        protected abstract void InsertRecord(Connection.Storage obj1, Connection.Storage obj2,
            object objId, ref List<Parameter> paramList);

        /// <summary>
        /// Retrieve an object by its name
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="paramList"></param>
        protected abstract void RetrieveRecordByName(Connection.Storage obj, ref List<Parameter> paramList);

        /// <summary>
        /// Retrieve an object by ID
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="paramList"></param>
        protected abstract void RetrieveRecordByID(Connection.Storage obj, ref List<Parameter> paramList);

        /// <summary>
        /// Returns all objects of a given type
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="creator"></param>
        /// <param name="results"></param>
        protected abstract void RetrieveAllRecords(Connection.Storage obj, Connection.ObjectCreator creator, out List<IObject> results);

        /// <summary>
        /// Implements client-side search for providers that do not support server-side search
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="text"></param>
        /// <param name="creator"></param>
        /// <param name="results"></param>
        protected abstract void ManualSearchRecords(Connection.Storage obj, string text, 
            Connection.ObjectCreator creator, out List<IObject> results);

        /// <summary>
        /// Updates an object with fresh data
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="paramList"></param>
        protected abstract void UpdateRecord(Connection.Storage obj, List<Parameter> paramList);

        /// <summary>
        /// Deletes an object
        /// </summary>
        /// <param name="objKind"></param>
        /// <param name="id"></param>
        protected abstract void DeleteRecord(string objKind, Guid id);

        /// <summary>
        /// Deletes an object
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="paramList"></param>
        protected abstract void DeleteRecord(Connection.Storage obj, List<Parameter> paramList);

        /// <summary>
        /// Used to return a DbDataReader when a query is executed
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        protected abstract DbDataReader ExecuteRetrieveReader(string query);

        /// <summary>
        /// Returns objects results of a query
        /// </summary>
        /// <param name="obj1"></param>
        /// <param name="sitkaQuery"></param>
        /// <param name="creator"></param>
        /// <param name="results"></param>
        protected abstract void ExecuteRetrieve(Connection.Storage obj1, string sitkaQuery,
            Connection.ObjectCreator creator, out List<IObject> results);

        /// <summary>
        /// Performs client-side joins for providers that do not support server-side joins
        /// </summary>
        /// <param name="action"></param>
        /// <param name="obj1"></param>
        /// <param name="obj2Type"></param>
        /// <param name="creator"></param>
        /// <param name="results"></param>
        protected abstract void ManualJoin(Connection.Action action, IObject obj1, Connection.Storage obj2Type,
            Connection.ObjectCreator creator, out List<IObject> results);

        #endregion
    }
}
